﻿using BingSpatialDataServiceExcelAddin.BSDSTools.Models;
using System;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Xml;
using System.Xml.Serialization;

namespace BingSpatialDataServiceExcelAddin.BSDSTools
{
    public class BatchGeocoder
    {
        #region Public Actions

        public Action<BatchGeocoderResults> GeocodeCompleted;

        public Action<string> ProgressChanged;

        #endregion

        #region Public Methods
        
        /// <summary>
        /// Method to geocode a set of data.
        /// </summary>
        /// <param name="dataFilePath"></param>
        /// <param name="dataFormat">Data type of: xml, csv, tab, pipe</param>
        /// <param name="bingMapsKey">Bing Maps key to use for accessing service.</param>
        public void Geocode(GeocodeFeed dataFeed, string bingMapsKey)
        {
            BackgroundWorker bw = new BackgroundWorker();
            bw.WorkerReportsProgress = true;

            bw.RunWorkerCompleted += (sender, args) =>
            {
                if (GeocodeCompleted != null)
                {
                    GeocodeCompleted((BatchGeocoderResults)args.Result);
                }
            };
            
            bw.ProgressChanged += (sender, args) =>
            {
                if (ProgressChanged != null)
                {
                    ProgressChanged(args.UserState as string);
                }
            };

            bw.DoWork += (sender, args)=>{
                try
                {
                    dataFeed.Version = "2.0";

                    bw.ReportProgress(0, "Creating Batch Geocode Job.");

                    string dataflowJobLocation = CreateJob(dataFeed, bingMapsKey);

                    //Continue to check the dataflow job status until the job has completed
                    DownloadDetails statusDetails = new DownloadDetails();
                    do
                    {
                        statusDetails = CheckStatus(dataflowJobLocation, bingMapsKey);
                        //Console.WriteLine("Dataflow Job Status: {0}", statusDetails.jobStatus);

                        if (statusDetails.JobStatus == "Aborted")
                        {
                            bw.ReportProgress(0, "Batch Geocode Job Aborted.");
                            throw new Exception("Job was aborted due to an error.");
                        }

                        if (statusDetails.JobStatus.Equals("Pending"))
                        {
                            Thread.Sleep(15000); //Get status every 15 seconds
                        }
                    }
                    while (statusDetails.JobStatus.Equals("Pending"));

                    bw.ReportProgress(0, "Downloading Batch Geocode results.");
                    BatchGeocoderResults results = DownloadResults(statusDetails, bingMapsKey);
                    
                    bw.ReportProgress(0, "Batch Geocode completed.");
                    args.Result = results;
                }
                catch (Exception e)
                {
                    args.Result = new BatchGeocoderResults()
                    {
                        Error = e.Message
                    };
                }
            };
            bw.RunWorkerAsync();
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Creates a geocode dataflow job and uploads spatial data to process.
        /// </summary>
        /// <param name="dataFeed">The address data to geocode.</param>
        /// <param name="bingMapsKey">The Bing Maps Key to use for this job. The same key is used to get job status and download results.</param>
        /// <param name="description">Text that is used to describe the geocode dataflow job. </param>
        /// <returns>A URL that defines the location of the geocode dataflow job that was created.</returns>
        private static string CreateJob(GeocodeFeed dataFeed, string bingMapsKey)
        {
            //Build the HTTP URI that will upload and create the geocode dataflow job
            Uri createJobUri = new Uri("https://spatial.virtualearth.net/REST/v1/dataflows/geocode?input=xml&key=" + Uri.EscapeUriString(bingMapsKey));

            //Include the data to geocode in the HTTP request
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(createJobUri);

            // The HTTP method must be 'POST'.
            request.Method = "POST";
            request.ContentType = "application/xml";

            XmlSerializer serializer = new XmlSerializer(typeof(GeocodeFeed));
            
            using (Stream requestStream = request.GetRequestStream())
            {
                using (XmlTextWriter xtWriter = new XmlTextWriter(requestStream, Encoding.UTF8))
                {
                    serializer.Serialize(xtWriter, dataFeed);
                }
            }

            //Submit the HTTP request and check if the job was created successfully. 
            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                // If the job was created successfully, the status code should be
                // 201 (Created) and the 'Location' header should contain a URL
                // that defines the location of the new dataflow job. You use this 
                // URL with the Bing Maps Key to query the status of your job.
                if (response.StatusCode != HttpStatusCode.Created)
                {
                    throw new Exception("An HTTP error status code was encountered when creating the geocode job.");
                }

                string dataflowJobLocation = response.GetResponseHeader("Location");
                if (String.IsNullOrEmpty(dataflowJobLocation))
                {
                    throw new Exception("The 'Location' header is missing from the HTTP response when creating a goecode job.");
                }

                return dataflowJobLocation;
            }
        }

        /// <summary>
        /// Checks the status of a dataflow job and defines the URLs to use to download results when the job is completed.
        /// </summary>
        /// <param name="dataflowJobLocation">The URL to use to check status for a job.</param>
        /// <param name="bingMapsKey">The Bing Maps Key for this job. The same key is used to create the job and download results.</param>
        /// <returns>
        /// A DownloadDetails object that contains the status of the geocode dataflow job (Completed, Pending, Aborted). 
        /// When the status is set to Completed, DownloadDetails also contains the links to download the results
        /// </returns>
        private static DownloadDetails CheckStatus(string dataflowJobLocation, string bingMapsKey)
        {
            DownloadDetails statusDetails = new DownloadDetails();
            statusDetails.JobStatus = "Pending";

            //Build the HTTP Request to get job status
            UriBuilder uriBuilder = new UriBuilder(dataflowJobLocation + @"?key=" + bingMapsKey + "&output=xml");
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uriBuilder.Uri);

            request.Method = "GET";

            //Submit the request and read the response to get job status and to retrieve the links for downloading the job results
            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                if (response.StatusCode != HttpStatusCode.OK)
                {
                    throw new Exception("An HTTP error status code was encountered when checking job status.");
                }

                using (Stream receiveStream = response.GetResponseStream())
                {
                    XmlTextReader reader = new XmlTextReader(receiveStream);
                    while (reader.Read())
                    {
                        if (reader.IsStartElement())
                        {
                            if (reader.Name.Equals("Status"))
                            {
                                //return job status
                                statusDetails.JobStatus = reader.ReadString();

                                return (statusDetails);
                            }
                            else if (reader.Name.Equals("Link"))
                            {
                                //Set the URL location values for retrieving 
                                // successful and failed job results
                                reader.MoveToFirstAttribute();
                                if (reader.Value.Equals("output"))
                                {
                                    reader.MoveToNextAttribute();
                                    if (reader.Value.Equals("succeeded"))
                                    {
                                        statusDetails.Succeededlink = reader.ReadString();

                                    }
                                    else if (reader.Value.Equals("failed"))
                                    {
                                        statusDetails.Failedlink = reader.ReadString();
                                    }
                                }
                            }
                        }
                    }

                }
            }

            return (statusDetails);
        }

        /// <summary>
        /// Downloads job results to files names Success.txt (successfully geocoded results) and Failed.txt (info about spatial data that was not geocoded successfully).
        /// </summary>
        /// <param name="statusDetails">Inclues job status and the URLs to use to download all geocoded results.</param>
        /// <param name="bingMapsKey">The Bing Maps Key for this job. The same key is used to create the job and get job status. </param>
        private static BatchGeocoderResults DownloadResults(DownloadDetails statusDetails, string bingMapsKey)
        {
            BatchGeocoderResults results = new BatchGeocoderResults();
            XmlSerializer serializer = new XmlSerializer(typeof(GeocodeFeed));

            //Write the results for data that was geocoded successfully to a file named Success.xml
            if (statusDetails.Succeededlink != null && !statusDetails.Succeededlink.Equals(String.Empty))
            {
                //Create a request to download successfully geocoded data. You must add the Bing Maps Key to the 
                //download location URL provided in the response to the job status request.
                UriBuilder successUriBuilder = new UriBuilder(statusDetails.Succeededlink + @"?key=" + bingMapsKey);
                HttpWebRequest successfulRequest = (HttpWebRequest)WebRequest.Create(successUriBuilder.Uri);

                successfulRequest.Method = "GET";

                using (HttpWebResponse response = (HttpWebResponse)successfulRequest.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception("An HTTP error status code was encountered when downloading results.");
                    }

                    using (Stream receiveStream = response.GetResponseStream())
                    {
                        results.Successful = (GeocodeFeed)serializer.Deserialize(receiveStream);
                    }
                }
            }

            //If some spatial data could not be geocoded, write the error information to a file called Failed.xml
            if (statusDetails.Failedlink != null && !statusDetails.Failedlink.Equals(String.Empty))
            {
                HttpWebRequest failedRequest = (HttpWebRequest)WebRequest.Create(new Uri(statusDetails.Failedlink + "?key=" + bingMapsKey));

                failedRequest.Method = "GET";

                using (HttpWebResponse response = (HttpWebResponse)failedRequest.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception("An HTTP error status code was encountered when downloading results.");
                    }

                    using (Stream receiveStream = response.GetResponseStream())
                    {
                        results.Failed = (GeocodeFeed)serializer.Deserialize(receiveStream);
                    }
                }
            }

            return results;
        }

        #endregion           
    }
}
